Skip to content

feat: sealed interface, covariant templates, and error-type composition#76

Merged
valbeat merged 6 commits into
mainfrom
feat/article-inspired-type-improvements
Jun 6, 2026
Merged

feat: sealed interface, covariant templates, and error-type composition#76
valbeat merged 6 commits into
mainfrom
feat/article-inspired-type-improvements

Conversation

@valbeat

@valbeat valbeat commented Jun 5, 2026

Copy link
Copy Markdown
Owner

概要

以下の記事で紹介されている PHPStan テクニックを取り込み、型注釈を改良します。

変更はすべて PHPDoc レベルでランタイム挙動は不変です(型の緩和方向なので後方互換)。

変更内容

@phpstan-sealed Ok|Err(記事1)

Result の実装を Ok/Err に限定。instanceof ベースの match (true) が網羅的と判定され、instanceof Ok の else 分岐が Err に絞り込まれます。

@template-covariant T / @template-covariant E(記事1)

Ok<T> (= Result<T, never>) と Err<E> (= Result<never, E>) が任意の Result<T, E> に代入可能になり、@return Result<User, DbError> の関数で return new Ok($user); がそのまま通ります。

エラー型の合成(記事2)

  • andThen() / and(): コールバック・引数が異なるエラー型 F を持てるようになり、戻り値は Result<U, E|F>
  • orElse() / or(): 異なる成功型 U を許容し、戻り値は Result<T|U, F>
// PHPStan が Result<User, ValidationError|NotFoundError> と推論
$user = validateUserId($request['id'])
    ->andThen(findUserById(...));

付随する型精度の修正

  • mapOr() の戻り値を U に修正(従来の T|U は不正確)
  • unwrapOrElse() を条件付き戻り値型 ($this is Ok<mixed> ? T : U)
  • 条件付き戻り値型の条件を Ok<T>Ok<mixed> に変更(共変テンプレートが不変位置に出現しないように)

テスト

  • tests/types/result.php を新規追加: PHPStan\Testing\assertType() による型レベルテスト(composer phpstan で検証される)
  • 異なる型を合成するチェーンのユニットテストを追加(69 tests / 111 assertions)
  • 共変化により定数型が保持される(new Ok(10)Ok<10>)ため、既存テストのリテラル比較が alwaysTrue/alwaysFalse になる箇所をヘルパーで widening

検証

  • composer phpstan (level max): No errors
  • phpunit: OK (69 tests, 111 assertions)
  • composer cs-check: パス

Note

@phpstan-sealed は phpstan 2.1.54(composer.json の固定バージョン)で利用可能です。ローカルの vendor が古い場合は composer update が必要です。

Summary by CodeRabbit

Documentation

  • 型安全性についてのドキュメントを新設し、エラー型合成、型ナローイング、値型保持などの機能を説明しました。

Tests

  • エラー型合成とエラー回復のテストケースを追加。型推論の正確性を検証するテストも新たに実装しました。

互換性への影響(静的解析レベル)

ランタイムは無変更ですが、PHPStan を使う利用側には以下の影響があり得ます:

  • @phpstan-sealed による実装制限: Result を独自実装している downstream コードは、アップグレード後に PHPStan エラーになります(Ok/Err 以外の実装を意図的に禁止するための変更です)
  • 定数型の保持: 共変化により new Ok(10)Ok<10> と推論されるため、level max ではリテラル比較が alwaysTrue/alwaysFalse として新たに検出されることがあります(README の Type Safety セクションに対処法を記載)

セルフレビューでの追加修正

  • 具象 Ok<T>/Err<E> レシーバで no-op メソッドが型を過剰に広げる問題を修正($ok->orElse(...)Result<int|string, never> でなく Ok<int> のままになるよう、クラス別オーバーライドを追加)
  • README の不正確な記述を修正(unwrapOr の Err 側は never でなくデフォルト値の型)
  • and/or 系のチャネル合成が Rust からの意図的な逸脱であることを README に明記

valbeat added 4 commits June 6, 2026 08:39
…ing, and error-type composition

Currently failing (RED) — documents the desired type-level behavior:
- Ok<T> assignable to Result<T, E> (covariance)
- andThen/and compose error types as E|F
- orElse/or compose success types as T|U
- instanceof Ok else-branch narrows to Err (sealed)
- mapOr returns U (not T|U)

Inspired by:
- https://zenn.dev/higaki/articles/my-php-result-type
- https://zenn.dev/higaki/articles/my-php-result-type-more
…sition

- Mark Result as @phpstan-sealed Ok|Err so PHPStan treats Ok/Err as
  exhaustive (match(true) over instanceof checks needs no default arm)
- Make T/E covariant (@template-covariant) so Ok<T> (= Result<T, never>)
  and Err<E> (= Result<never, E>) are assignable to any Result<T, E>
- andThen/and now accept callbacks/results with a different error type F
  and compose error types as Result<U, E|F>
- orElse/or now accept a different success type U and compose success
  types as Result<T|U, F>
- Fix mapOr return type (U, not T|U) and tighten unwrapOrElse to a
  conditional return type
- Conditional return types now test against Ok<mixed>/Err<mixed> to keep
  covariant templates out of invariant positions
- Widen literal values via helpers in tests: covariant templates preserve
  constant types (new Ok(10) is Ok<10>), which made literal comparisons
  always-true/false at PHPStan level max

Inspired by:
- https://zenn.dev/higaki/articles/my-php-result-type
- https://zenn.dev/higaki/articles/my-php-result-type-more
Exercise the widened signatures at runtime: an Ok chained into a
callback that can fail with a different error type, an Err passing
through andThen unchanged, and an Err recovered into a different
success type via orElse.
@valbeat valbeat self-assigned this Jun 5, 2026
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Too many files changed? Review this PR in Change Stack to see how the pieces fit before you dive in.

Review Change Stack

Warning

Review limit reached

@valbeat, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 14 minutes and 31 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 634c516f-6ea7-4872-acdf-9098d4195386

📥 Commits

Reviewing files that changed from the base of the PR and between d383634 and f40431d.

📒 Files selected for processing (4)
  • README.md
  • src/Err.php
  • src/Ok.php
  • tests/Types/result.php

Walkthrough

このPRは PHPStan との連携により、Result 型の型安全性を段階的に強化します。共変ジェネリクスと sealed インターフェース宣言を導入し、unwrap 系・and/or メソッドの戻り値型を条件付き型や複合型で精密化。ドキュメント、ランタイムテスト、静的型検証の三層で動作と型推論を検証します。

Changes

PHPStan 型安全性強化

Layer / File(s) Summary
共変性と sealed インターフェース宣言
src/Ok.php, src/Err.php, src/Result.php
Ok・Err・Result で @template@template-covariant へ変更。Result に @phpstan-sealed Ok|Err 注釈を追加し、型パラメータの共変性と網羅性を PHPStan に宣言。
条件付き型による unwrap メソッド推論
src/Result.php, src/Ok.php
unwrap / unwrapErr / unwrapOr / unwrapOrElse の PHPDoc 戻り値を条件付き型へ更新。mapOr の戻り値を T|U から U へ修正。unwrapOr@paramU に拡張。
and/or メソッドのエラー型合成型推論
src/Result.php
and / andThen でエラー型 E|F を合成、or / orElse で成功型 T|U を合成する型注釈へ更新。第2引数のエラー型 F パラメータを明示。
Type Safety ドキュメントと実例
README.md
「Type Safety」セクション追加。sealed、共変テンプレート、ワイドニング、ナローイング、定数値型保持を説明。「Chaining Operations」セクションに andThen() エラー型合成の実例を追加。
ランタイムテスト:エラー型合成と復旧検証
tests/ErrTest.php, tests/OkTest.php
andThen / orElse が異なるエラー型・成功型を扱う新テストを追加。リテラル型保持用の asInt() / asString() ヘルパを導入。異なるエラー型でも元の値が保持される、orElse で復旧できることを実証。
PHPStan 型推論検証テスト
tests/types/result.php
parsePositiveInt / findNameById を用いた assertType ベースの検証テスト群を追加。エラー型・成功型合成、型ナローイング、網羅性、unwrap 系・map 系の戻り値型推論を確認。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

根拠:

  • PHPStan 型注釈の変更が複数ファイルにわたり、共変性・sealed・条件付き型など型理論的な正確さの検証が必要
  • Result インターフェース(中核型)のジェネリクス変更により、整合性確認が重要
  • ランタイムテスト・静的型検証テストの双方で動作と型推論を確認する必要があり、検証負荷は中程度

Possibly related PRs

  • valbeat/php-result#7: src/Result.php の PHPStan 型注釈(isOk()/isErr() ナローイングと unwrap* 条件付き型)を同様に改善。このPRの前段階の型推論改善と関連。
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed PR タイトルは「sealed interface, covariant templates, and error-type composition」という3つの主要な変更を正確に要約しており、PR の目的(PHPStan型改善の導入)と提供された修正内容と完全に一致しています。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/article-inspired-type-improvements

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (796082c) to head (f40431d).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@             Coverage Diff             @@
##                main       #76   +/-   ##
===========================================
  Coverage     100.00%   100.00%           
  Complexity        40        40           
===========================================
  Files              2         2           
  Lines             80        80           
===========================================
  Hits              80        80           
Flag Coverage Δ
unittests 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances the library's type safety and integration with PHPStan by introducing covariant template parameters (@template-covariant), sealing the Result interface with @phpstan-sealed Ok|Err, and refining method signatures to support precise success and error type composition (e.g., union types in and, andThen, or, and orElse). Additionally, the documentation in README.md has been updated to explain these type-safety features, and comprehensive type tests have been added to verify PHPStan's type narrowing and inference behavior. No review comments were provided, so I have no feedback to address.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@valbeat valbeat marked this pull request as ready for review June 6, 2026 00:28
Copilot AI review requested due to automatic review settings June 6, 2026 00:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the library’s PHPStan-level type precision (no runtime behavior changes) by adopting a sealed Result interface, covariant templates, and composable success/error types for chaining APIs, along with updated docs and type-level tests to validate the new typing behavior.

Changes:

  • Introduce @phpstan-sealed Ok|Err and make Result/Ok/Err templates covariant to improve assignability and narrowing.
  • Update PHPDoc generics for andThen()/and() and orElse()/or() to compose error/success types as unions; refine conditional return types and fix mapOr() return type.
  • Add PHPStan assertType() tests and adjust PHPUnit tests to avoid literal-type always-true/false issues caused by covariance preserving constant types.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Result.php Adds sealed interface + covariant templates; refines conditional return types; updates composition typing for chaining methods.
src/Ok.php Marks T covariant and aligns unwrapOr() PHPDoc with the updated generic contract.
src/Err.php Marks E covariant to match the updated Result variance model.
tests/types/result.php Adds PHPStan type-level coverage for narrowing, sealed exhaustiveness, and type composition.
tests/OkTest.php Adds runtime tests for andThen() composing different error types; introduces helper to widen literal ints.
tests/ErrTest.php Adds runtime tests for composition behaviors; introduces helpers to widen literal ints/strings.
README.md Documents the new PHPStan type-safety features and illustrates error-type composition in chaining.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread README.md Outdated
Comment thread tests/Types/result.php
Self-review finding: the widened interface signatures leaked impossible
types when the receiver is statically Ok<T> or Err<E> — e.g.
$ok->orElse(fn => Ok<string>) inferred Result<int|string, never> even
though Ok::orElse always returns $this, and Ok::mapOr unified the
default's type into the result even though Ok never returns the default.

Add per-class PHPDoc overrides on the no-op/passthrough sides:
- Ok: map => Ok<U>; mapErr/inspect/inspectErr/or/orElse => $this;
  mapOr/mapOrElse split the default's template from the callback's
- Err: mapErr => Err<F>; map/inspect/inspectErr/and/andThen => $this;
  mapOr/mapOrElse split likewise

Covered by new assertType cases for concrete receivers in
tests/types/result.php.

Also fix the README Type Safety section: unwrapOr/unwrapOrElse resolve
to the default's type on Err (not never), and note that the and/or
channel widening deliberately diverges from Rust.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 89-103: Provide a runnable PHP 8.4+ example by adding minimal
implementations and type definitions referenced in the snippet: declare the
Result generic type (or import it), define classes/structs UserId, User,
ValidationError, NotFoundError, and implement validateUserId(string $raw):
Result and findUserById(UserId $id): Result with concrete return values (e.g.
return new Ok(new UserId($raw)) or return new Err(new NotFoundError())) so the
chain ($user = validateUserId($request['id'])->andThen(findUserById(...));)
actually executes; update the snippet to include these type/class definitions
and simple return statements so readers can copy and run it.

In `@tests/types/result.php`:
- Around line 1-154: Add a test covering nested Result types: create a function
(e.g., testNestedResultFlattening) that accepts Result<Result<int,
RuntimeException>, LogicException> and calls ->andThen with a callback returning
the inner Result to flatten; then assertType that the result is Result<int,
LogicException|RuntimeException>, ensuring the case checks how andThen
composes/propagates inner and outer error types (reference symbols:
testNestedResultFlattening, Result, andThen, assertType).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 20ae8024-60ab-42b9-8e17-b763bc68b7e6

📥 Commits

Reviewing files that changed from the base of the PR and between 796082c and d383634.

📒 Files selected for processing (7)
  • README.md
  • src/Err.php
  • src/Ok.php
  • src/Result.php
  • tests/ErrTest.php
  • tests/OkTest.php
  • tests/types/result.php

Comment thread README.md
Comment thread tests/Types/result.php
- Rename tests/types/ to tests/Types/ to match the namespace casing
  (Copilot)
- Make the README error-composition example runnable on PHP 8.4
  (CodeRabbit)
- Add a nested-Result flattening type test for andThen (CodeRabbit)
@valbeat valbeat merged commit a8a6d23 into main Jun 6, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants